import os
import sys
import stat
import shutil
import logging
import simplejson
import subprocess


from command_parser.option_filter_base import FilterBase
from command_parser.option_base import *
from command_parser.gcc_parse.gcc_option_dict import OptionCategory
from utils.common_utils import *
from utils.file_type_detect import *


def get_gcc_include_path_search_order(compiler=""):
    if not compiler:
        return
    
    lang = "c"
    if is_cpp_compiler(compiler=compiler):
        lang = "c++"
    command = [compiler, "-E", "-Wp,-v", "-x", lang, "/dev/null"]
    
    p = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    _, std_err = p.communicate()
    lines = std_err.decode().splitlines()
    idx = 0
    for idx in range(0, len(lines)):
        if "#include <...> search starts here" in lines[idx]:
            break
    search_paths = lines[idx+1:-1]
    
    search_paths = [path.strip() for path in search_paths]
    
    return search_paths
    
    

class GCCFilter(FilterBase):
    
    def __init__(self, context=None):
        self.__context = context
    
        
    def append_option_output(self, cwd='', compiler='', options=[], output_file=""):
        new_option = Option(option="-o", parameter=[output_file],type=PREFIX_SPACE)
        options.append(new_option)
        
        
    def filter_original_options(self, cwd='', compiler='', options=[]):

        filter_dict = {
            '-': self.handle_option_stdin,
            '--shared': self.handle_option_doushared,
        }

        self.filter_func_callback(cwd=cwd, compiler=compiler, options=options, func_dict=filter_dict)
    
    def filter_link_options(self, options=[]):
        
        filter_options = []
        for option in options:
            
            if option.category == OptionCategory.LINKER:
                filter_options.append(option)
        
        self.remove_options_by_list(raw_options=options, remove_list=filter_options)
    
    def filter_compile_options(self, options=[]):
        
        filter_options = ['-x']
        for option in options:
            
            if option.category == OptionCategory.OPTIMIZATION \
                or option.category == OptionCategory.WARNING:
                    filter_options.append(option)
        
        self.remove_options_by_list(raw_options=options, remove_list=filter_options)
    
    def filter_options_default(self, options=[]):
        
        filter_options = []
        
        for option in options:
            
            if option.attribute["action"] == "filter":
                filter_options.append(option)
                continue
            
            if option.category == OptionCategory.DEBUGGING \
                or option.category == OptionCategory.WARNING \
                or option.category == OptionCategory.LINKER \
                or option.category == OptionCategory.OPTIMIZATION:
                    filter_options.append(options)
                    
        self.remove_options_by_list(raw_options=options, remove_list=filter_options)
            
    
    def handle_option_stdin(self, cwd="", compiler="", options="", option=None):
        
        src_md5 = str_md5(str(os.getpid()))
        content = ""
        f_mode = os.fstat(sys.stdin.fileno()).st_mode
        if stat.S_ISFIFO(f_mode) or stat.S_ISREG(f_mode) or stat.S_ISCHR(f_mode):
            content = sys.stdin.readlines()
            content = ''.join(content)
            content += str(os.getpid())
            src_md5 = str_md5(content)
        
        if not os.path.exists(self.__context.CACHE_DIR):
            os.mkdir(self.__context.CACHE_DIR)
        
        stdin_cache_file = os.path.join(self.__context.CACHE_DIR, src_md5 + ".cache")
        with open(stdin_cache_file, "w") as f:
            f.write(content)
        
        option.option = "" 
        option.parameter[0] = stdin_cache_file
        option.type = PREFIX_MATCH
        option.category = OptionCategory.SOURCE_FILE
        option.keep = True
    
    
    def handle_option_doushared(self, cwd="", compiler="", options=[], option=None):      
        option.keep = False
            
    def filter_command(self, options=[]):
        
        filter_options = []
        for option in options:
            if not option.keep:
                filter_options.append(option)
    
        self.remove_options_by_list(raw_options=options, remove_list=filter_options)
    
    def remove_options_by_list(self, raw_options=[], remove_list=[]):
        
        new_options = []
        for option in raw_options:
            if option not in remove_list:
                new_options.append(option)
        
        raw_options[0:] = new_options
    
    
    def handle_option_std(self, cwd="", compiler="", options=[], option=None):
        
        cc_ex_iso_std = ['iso9899:1990', 'iso9899:199409', 'iso9899:1999', 'iso9899:199x', 'iso9899:2011']
        cc_iso_std = ['c90', 'c89', 'c99', 'c9x', 'c11', 'c1x']
        
        c_plus_iso_std = ['c++98', 'c++03', 'c++0x', 'c++11', 'c++14', 'c++1y']
        
        gnu_std_cc_to_plus = {
            "gnu89": "gnu++98",
            "gnu90": "gnu++98",
            "gnu9x": "gnu++98",
            "gnu99": "gnu++0x",
            "gnu11": "gnu++11",
            "gnu1x": "gnu++1y"
        }
        
        gnu_std_plus_to_c = {
            'gnu++98': "gnu9x",
            'gnu++03': "gnu99",
            'gnu++0x': "gnu99",
            'gnu++11': "gnu11",
            'gnu++14': "gnu1x",
            'gnu++1y': "gnu1x"
        }
        
        if not option.parameter:
            return
        
        standard = option.parameter[0]
        
        para_list = option.parameter[0].split(' ')
        
        for para in para_list:
            if para in cc_ex_iso_std \
                or para in cc_iso_std \
                or para in c_plus_iso_std \
                or para in gnu_std_cc_to_plus.keys() \
                or para in gnu_std_plus_to_c.keys():
                standard = para
                break
        
        ext = ''        
        for option in options:
            if option.option == "-x" and option.parameter:
                type = option.parameter[0]
                if type in ["c", "c++"]:
                    ext = type
                    break
            
            if not option.option and option.parameter:
                ext = os.path.splitext(option.parameter[0])[1].lstrip()
        
        std_type = "c++" if is_cpp_compiler(compiler=compiler) else "c"
        
        if ext:
            std_type = EXTS_LANG.get(ext, std_type)
        
        
        new_std = standard
        if std_type == "c++" or std_type == "objective-c++":
            idx = -1
            if standard in cc_iso_std:
                idx = cc_iso_std.index(std_type)
                new_std = c_plus_iso_std[idx]
            
            if idx == -1 and standard in gnu_std_cc_to_plus.keys():
                new_std = gnu_std_cc_to_plus.get(std_type)
        
        elif std_type == "c" or std_type == "objective-c":
            idx = -1
            if standard in c_plus_iso_std:
                idx = c_plus_iso_std.index(standard)
                new_std = cc_iso_std[idx]
            
            if standard in gnu_std_plus_to_c:
                new_std = gnu_std_plus_to_c[standard]
        
        option.parameter = [new_std]
    
    def handle_option_fpie(self, cwd="", compiler="", options=[], option=None):
        option.option = '-fPIE'
        return 

    def handle_option_mcpu(self, cwd="", compiler="", options=[], option=None):
        
        if not os.path.exists(self.__context.ARCH_DICT):
            return
         
        with open(self.__context.ARCH_DICT, "r") as f:
            arch_dict = simplejson.load(f)["cpu_map"]
        
        raw_arch = option.parameter[0]
        new_target = arch_dict[raw_arch]
        
        option.option = "-target"
        option.parameter = [new_target]
        option.type = PREFIX_SPACE
        return

    def handle_option_include(self, cwd="", compiler="", options=[], option=None):
        
        if not option.parameter:
            return
        
        pre_head_ext = ['.gch', '.pch', '.PCH', '.GCH']
        
        include_path = option.parameter[0]
        if not os.path.isabs(include_path):
            include_path = os.path.join(cwd, include_path)
        
        for ext in pre_head_ext:
            if os.path.exists(include_path + ext):
                if os.path.exists(include_path):
                    os.remove(include_path + ext)
                else:
                    option.keep = False
                    break
            
    def append_option_flto(self, cwd='', compiler='', options=[], option=None):
        
        option = Option(option="-flto", type=PREFIX_SIMPLE)
        options.append(option)
    
    
    def append_option_fpic(self, cwd='', compiler='', options=[], option=None):
        
        option = Option(option="-fpic", type=PREFIX_SIMPLE)
        options.append(option)
    
    
    def append_option_target(self, cwd='', compiler='', options=[], option=None):
        new_option = Option(option="-target", parameter=["aarch64-pc-linux-gnu"], type=PREFIX_SPACE)
        options.append(new_option)
    
    
    def append_option_bin32(self, cwd='', compiler='', options=[], option=None):
        pass
    
    
    def append_clang_spec_options(self, cwd='', compiler='', options=[], option=None):

        inline_options = [('-fheinous-gnu-extensions', []),
                          ('-fno-inline-functions', []),
                          ('-mllvm', ['-inline-threshold=0']),
                          ('-mllvm', ['-inlinehint-threshold=0'])]
        
        spec_options = []
        
        for i_opt, i_para in inline_options:
            if i_para:
                spec_opt = Option(option=i_opt, parameter=i_para, type=PREFIX_SPACE)
            else:
                spec_opt = Option(option=i_opt, type=PREFIX_SPACE)
            spec_options.append(spec_opt)
        
        options.extend(spec_options)
    
    def append_option_compile(self, cwd="", compiler="", options=[], option=None):
        
        option = Option(option="-c", type=PREFIX_SIMPLE)
        options.append(option)
    
    def append_option_debug(self, cwd="", compiler="", options=[], option=None):
        
        option = Option(option="-g", type=PREFIX_SIMPLE)
        options.append(option)
    
    def append_option_wno_error(self, cwd="", compiler="", options=[], option=None):
        
        option = Option(option="-Wno-everything", type=PREFIX_SIMPLE)
        options.append(option)
        
    def filter_transform_option(self, cwd="", compiler="", options=[], option=None):
        
        filter_dict ={
            '-pipe': self.filter_option_removal,
            '-fvisibility': self.filter_option_removal,
            '-Wp,': self.filter_option_removal,

            # Can't process assemble code
            '-Wa,': self.filter_option_removal,
            '-Xassembler': self.filter_option_removal,
            '-Werror': self.filter_option_removal,

            # Remove -M  -MD -MMD -MM  -MF  -MG  -MP  -MQ  -MT
            '-M': self.filter_option_removal,
            '-MD': self.filter_option_removal,
            '-MMD': self.filter_option_removal,
            '-MM': self.filter_option_removal,
            '-MF': self.filter_option_removal,
            '-MG': self.filter_option_removal,
            '-MP': self.filter_option_removal,
            '-MQ': self.filter_option_removal,
            '-MT': self.filter_option_removal,

            '-nostdinc++': self.filter_option_removal,
            '-nostdlib': self.filter_option_removal,
            '-nostdinc': self.filter_option_removal,
            
            'specs': self.handle_option_specs,
            '-march': self.filter_option_removal,

            # For compile-link command, we can not remove it by the category of directory.
            '-L': self.filter_option_removal
        }
        
        self.filter_func_callback(cwd=cwd, compiler=compiler, options=options, func_dict=filter_dict)
    
    def append_option_clang_ast(self, cwd="", compiler="", options=[], option=None):
        new_option = Option(option="-emit-ast", type=PREFIX_SIMPLE)
        options.append(new_option)
    
    def append_option_compile(self, cwd="", compiler="", options=[], option=None):
        if not options:
            return 
        
        new_opt = Option(option="-c", type=PREFIX_SIMPLE)
        options.append(new_opt)
    
    def append_option_include_custom_path(self, cwd="", compiler="", options=[], option=None):
        custom_include_paths = self.__context.CUSTOM_INCLUDE_PATH
        for include_path in custom_include_paths:
            new_option = Option(option="-isystem", parameter=[include_path], type=PREFIX_SPACE)
            options.append(new_option)
        
    def append_option_include_path(self, cwd="", compiler="", options=[], option=None):
        
        gcc_search_paths = get_gcc_include_path_search_order(compiler=compiler)
        
        new_options = []
        for path in gcc_search_paths:
            new_option = Option(option="-isystem", parameter=[path], type=PREFIX_SPACE)
            new_options.append(new_option)
        
        options.extend(new_options)
    
    def handle_option_specs(self, cwd="", compiler="", options=[], option=None):
        option.type = PREFIX_EQUAL
    
    def append_option_U(self, cwd="", compiler="", options=[], option=None):
        if not options:
            return
        
        opt = Option("-U", parameter=["__SIZEOF_INT128__"], type=PREFIX_SPACE)
        options.append(opt)